/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.gitee.huangjianbo.supos.openapi.httpclient5;

import io.gitee.huangjianbo.supos.openapi.core.SuposApiClient;
import io.gitee.huangjianbo.supos.openapi.core.config.SuposApiClientConfig;
import io.gitee.huangjianbo.supos.openapi.core.errors.ApiClientException;
import io.gitee.huangjianbo.supos.openapi.core.errors.InvalidArgumentsException;
import io.gitee.huangjianbo.supos.openapi.core.headers.CommonHeaders;
import io.gitee.huangjianbo.supos.openapi.core.http.*;
import io.gitee.huangjianbo.supos.openapi.core.http.request.MultipartFormDataPair;
import io.gitee.huangjianbo.supos.openapi.core.http.request.RequestArguments;
import io.gitee.huangjianbo.supos.openapi.core.http.request.types.RFile;
import io.gitee.huangjianbo.supos.openapi.core.http.response.ListResult;
import io.gitee.huangjianbo.supos.openapi.core.http.response.PageResult;
import io.gitee.huangjianbo.supos.openapi.core.http.response.Result;
import io.gitee.huangjianbo.supos.openapi.core.thread.ClientContext;
import io.gitee.huangjianbo.supos.openapi.core.thread.ClientContextWrapper;
import io.gitee.huangjianbo.supos.openapi.httpclient5.httpclient.HttpClient5Manager;

import org.apache.hc.client5.http.classic.methods.*;
import org.apache.hc.client5.http.entity.mime.*;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.HeaderGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

/**
 * @author tomcat
 * @version 1.0
 */
public abstract class AbstractHttpClient5SuposApiClient implements SuposApiClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHttpClient5SuposApiClient.class);

    protected SuposApiClientConfig globalConfig;

    private final HttpClient5Manager httpClientManager;

    public AbstractHttpClient5SuposApiClient() {
        this.httpClientManager = new HttpClient5Manager();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            LOGGER.debug("Translation interface client instance when system exits");
            this.httpClientManager.close();
        }));
        LOGGER.debug("The initialization interface client instance is based on HttpClient5");
    }

    @Override
    public SuposApiClientConfig getGlobalConfig() {
        return this.globalConfig;
    }

    @Override
    public void setGlobalConfig(SuposApiClientConfig config) {
        this.globalConfig = config;
        this.httpClientManager.setGlobalConfig(config);
    }

    /**
     * 获取Http Client实例
     *
     * @param context 客户端请求上下文
     * @return HttpClient
     */
    protected CloseableHttpClient getHttpClient(ClientContext context) {
        return this.httpClientManager.getClient(this.globalConfig.getEndpoint());
    }

    /**
     * 设置请求头
     *
     * @param header
     * @param httpRequest
     */
    protected void setHttpRequestHeaders(HttpHeader header, HeaderGroup httpRequest) {
        if (header != null && !header.isEmpty()) {
            header.toMap().forEach((k, v) -> httpRequest.addHeader(new BasicHeader(k, v)));
        }
    }

    /**
     * 请求是否成功（2xx）
     *
     * @param httpStatus 状态码
     * @return 是或否
     */
    protected boolean isSuccess(int httpStatus) {
        return httpStatus / 100 == 2;
    }

    /**
     * 请求是否失败（4xx、5xx）
     *
     * @param httpStatus 状态码
     * @return 是或否
     */
    protected boolean isFailure(int httpStatus) {
        return (httpStatus / 100 == 4) || (httpStatus / 100 == 5);
    }

    /**
     * 打印请求日志
     *
     * @param httpRequest 请求
     * @param body        请求体
     */
    private void log(BasicClassicHttpRequest httpRequest, String body) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("=> {} {}", httpRequest.getMethod(), httpRequest.getPath());
            Arrays.stream(httpRequest.getHeaders()).forEach(h -> LOGGER.debug("=> {}: {}", h.getName(), h.getValue()));
            LOGGER.debug("=> {}", body);
        }
    }

    /**
     * 打印响应日志
     *
     * @param response 响应
     * @param body     响应体
     */
    protected void log(ClassicHttpResponse response, String body) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("<= {}", response.getCode());
            Arrays.stream(response.getHeaders()).forEach(h -> LOGGER.debug("<= {}: {}", h.getName(), h.getValue()));
            LOGGER.debug("<= {}", body);
        }
    }

    /**
     * 构建请求体
     *
     * @param context 请求上下文
     * @param path    请求路径
     * @param args    请求实体参
     * @param header  请求头
     * @return HttpRequest
     */
    protected BasicClassicHttpRequest buildHttpRequest(ClientContext context, HttpPath path, RequestArguments args, HttpHeader header) {
        final String url = context.getEndpoint().toString() + args.toHttpPath(path).toString() + args.toQueryString();
        final String body = args.toBodyJsonString();
        BasicClassicHttpRequest httpRequest = null;
        switch (path.getMethod()) {
            case GET: {
                httpRequest = new HttpGet(url);
                break;
            }
            case POST: {
                httpRequest = new HttpPost(url);
                break;
            }
            case PUT: {
                httpRequest = new HttpPut(url);
                break;
            }
            case DELETE: {
                httpRequest = new HttpDelete(url);
                break;
            }
            case OPTIONS: {
                httpRequest = new HttpOptions(url);
                break;
            }
        }
        setHttpRequestHeaders(header, httpRequest);
        if (!httpRequest.containsHeader(HttpHeaders.CONTENT_TYPE)) {
            httpRequest.addHeader(new BasicHeader(HttpHeaders.CONTENT_TYPE, HttpContentType.APPLICATION_JSON.getMimeType()));
        }
        httpRequest.setEntity(new StringEntity(body));
        log(httpRequest, body);

        return httpRequest;
    }

    @Override
    public <R, T extends RequestArguments> R callWithResponse(HttpPath path, T args, HttpHeader header, Class<R> clazz) {
        if (!args.validate()) {
            throw new InvalidArgumentsException("The request arguments is invalid");
        }
        try (ClientContext context = new ClientContextWrapper(globalConfig)) {
            CloseableHttpClient httpClient = this.getHttpClient(context);
            BasicClassicHttpRequest httpRequest = buildHttpRequest(context, path, args, header);
            return execute(httpClient, httpRequest, clazz);
        } catch (ApiClientException e) {
            throw e;
        }  catch (Exception e) {
            throw new ApiClientException("Fail to call supOS service", e);
        }
    }

    @Override
    public <R, T extends RequestArguments> Result<R> callWithResponseForResult(HttpPath path, T args, HttpHeader header, Class<R> clazz) {
        if (!args.validate()) {
            throw new InvalidArgumentsException("The request arguments is invalid");
        }
        try (ClientContext context = new ClientContextWrapper(globalConfig)) {
            CloseableHttpClient httpClient = this.getHttpClient(context);
            BasicClassicHttpRequest httpRequest = buildHttpRequest(context, path, args, header);
            return executeReturnResult(httpClient, httpRequest, clazz);
        } catch (ApiClientException e) {
            throw e;
        }  catch (Exception e) {
            throw new ApiClientException("Fail to call supOS service", e);
        }
    }

    @Override
    public <R, T extends RequestArguments> ListResult<R> callWithResponseForListResult(HttpPath path, T args, HttpHeader header, Class<R> clazz) {
        if (!args.validate()) {
            throw new InvalidArgumentsException("The request arguments is invalid");
        }
        try (ClientContext context = new ClientContextWrapper(globalConfig)) {
            CloseableHttpClient httpClient = this.getHttpClient(context);
            BasicClassicHttpRequest httpRequest = buildHttpRequest(context, path, args, header);
            return executeReturnListResult(httpClient, httpRequest, clazz);
        } catch (ApiClientException e) {
            throw e;
        } catch (Exception e) {
            throw new ApiClientException("Fail to call supOS service", e);
        }
    }

    @Override
    public <R, T extends RequestArguments> PageResult<R> callWithResponseForPageResult(HttpPath path, T args, HttpHeader header, Class<R> clazz) {
        if (!args.validate()) {
            throw new InvalidArgumentsException("The request arguments is invalid");
        }
        try (ClientContext context = new ClientContextWrapper(globalConfig)) {
            CloseableHttpClient httpClient = this.getHttpClient(context);
            BasicClassicHttpRequest httpRequest = buildHttpRequest(context, path, args, header);
            return executeReturnPageResult(httpClient, httpRequest, clazz);
        } catch (ApiClientException e) {
            throw e;
        }  catch (Exception e) {
            throw new ApiClientException("Fail to call supOS service", e);
        }
    }

    @Override
    public <T extends RequestArguments> void callWithoutResponse(HttpPath path, T args, HttpHeader header) {
        if (!args.validate()) {
            throw new InvalidArgumentsException("The request arguments is invalid");
        }
        try (ClientContext context = new ClientContextWrapper(globalConfig)) {
            CloseableHttpClient httpClient = this.getHttpClient(context);
            BasicClassicHttpRequest httpRequest = buildHttpRequest(context, path, args, header);
            execute(httpClient, httpRequest);
        } catch (ApiClientException e) {
            throw e;
        }  catch (Exception e) {
            throw new ApiClientException("Fail to call supOS service", e);
        }
    }

    /**
     * 构建multipart/form-data请求体
     *
     * @param context 客户端上下文
     * @param path    请求路径
     * @param file    上传的文件
     * @param args    请求的参数
     * @param header  请求头
     * @return 请求体
     */
    protected BasicClassicHttpRequest buildMultipartHttpRequest(ClientContext context, HttpPostPath path, RFile file,
                                                                RequestArguments args, HttpHeader header) {
        final String url = context.getEndpoint().toString() + args.toHttpPath(path).toString() + args.toQueryString();
        HttpPost httpPost = new HttpPost(url);
        setHttpRequestHeaders(header, httpPost);

        HttpHeader contentTypeHeader = CommonHeaders.buildMultipartContentType();
        httpPost.setHeader(new BasicHeader(HttpHeaders.CONTENT_TYPE, contentTypeHeader.get(HttpHeaders.CONTENT_TYPE)));

        ContentType contentType = ContentType.parse(contentTypeHeader.get(HttpHeaders.CONTENT_TYPE));
        MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
                .setMode(HttpMultipartMode.EXTENDED)
                .setBoundary(contentType.getParameter(HttpHeaders.BOUNDARY));

        List<MultipartFormDataPair> formData = args.toMultipartFormData();
        if (formData != null && !formData.isEmpty()) {
            formData.forEach(data -> {
                ContentType ct = ContentType.parse(data.getContentType().getMimeType());
                ContentBody body = new StringBody(data.getValue(), ct);
                entityBuilder.addPart(data.getName(), body);
            });
        }

        ContentBody fileBody = new FileBody(file.toFile(), contentType);
        entityBuilder.addPart("file", fileBody);

        httpPost.setEntity(entityBuilder.build());

        return httpPost;
    }

    @Override
    public <R, T extends RequestArguments> R upload(HttpPostPath path, RFile file, T args, HttpHeader header, Class<R> clazz) {
        if (!file.validate() || !args.validate()) {
            throw new InvalidArgumentsException("The request arguments is invalid");
        }
        try (ClientContext context = new ClientContextWrapper(globalConfig)) {
            CloseableHttpClient httpClient = this.getHttpClient(context);
            BasicClassicHttpRequest httpRequest = buildMultipartHttpRequest(context, path, file, args, header);
            return execute(httpClient, httpRequest, clazz);
        } catch (ApiClientException e) {
            throw e;
        }  catch (Exception e) {
            throw new ApiClientException("Fail to call supOS service", e);
        }
    }

    @Override
    public <T extends RequestArguments> void upload(HttpPostPath path, RFile file, T args, HttpHeader header) {
        if (!file.validate() || !args.validate()) {
            throw new InvalidArgumentsException("The request arguments is invalid");
        }
        try (ClientContext context = new ClientContextWrapper(globalConfig)) {
            CloseableHttpClient httpClient = this.getHttpClient(context);
            BasicClassicHttpRequest httpRequest = buildMultipartHttpRequest(context, path, file, args, header);
            execute(httpClient, httpRequest);
        } catch (ApiClientException e) {
            throw e;
        }  catch (Exception e) {
            throw new ApiClientException("Fail to call supOS service", e);
        }
    }

    /**
     * 执行请求
     *
     * @param httpClient  客户端
     * @param httpRequest 请求
     * @param clazz       响应类型
     * @param <R>         返回参类型
     * @return 返回参
     */
    protected abstract <R> R execute(CloseableHttpClient httpClient, BasicClassicHttpRequest httpRequest, Class<R> clazz) throws IOException;

    protected abstract <R> Result<R> executeReturnResult(CloseableHttpClient httpClient, BasicClassicHttpRequest httpRequest, Class<R> clazz) throws IOException;

    protected abstract <R> ListResult<R> executeReturnListResult(CloseableHttpClient httpClient, BasicClassicHttpRequest httpRequest, Class<R> clazz) throws IOException;

    protected abstract <R> PageResult<R> executeReturnPageResult(CloseableHttpClient httpClient, BasicClassicHttpRequest httpRequest, Class<R> clazz) throws IOException;

    /**
     * 执行请求
     *
     * @param httpClient  客户端
     * @param httpRequest 请求
     */
    protected abstract void execute(CloseableHttpClient httpClient, BasicClassicHttpRequest httpRequest) throws IOException;
}
